// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2017 Intel Corporation. All Rights Reserved.

#include "fw-update-helper.h"
#include "model-views.h"
#include "viewer.h"
#include <realsense_imgui.h>
#include "ux-window.h"

#include <rsutils/os/special-folder.h>
#include "os.h"

#include <rsutils/easylogging/easyloggingpp.h>
#include <map>
#include <vector>
#include <string>
#include <thread>
#include <condition_variable>

#ifdef INTERNAL_FW
#include "common/fw/D4XX_FW_Image.h"
#else
#define FW_D4XX_FW_IMAGE_VERSION ""
const char* fw_get_D4XX_FW_Image(int) { return NULL; }


#endif // INTERNAL_FW

constexpr const char* recommended_fw_url = "https://dev.intelrealsense.com/docs/firmware-updates";

namespace rs2
{
    enum firmware_update_ui_state
    {
        RS2_FWU_STATE_INITIAL_PROMPT = 0,
        RS2_FWU_STATE_IN_PROGRESS = 1,
        RS2_FWU_STATE_COMPLETE = 2,
        RS2_FWU_STATE_FAILED = 3,
    };

    bool is_recommended_fw_available(const std::string& product_line, const std::string& pid)
    {
        auto pl = parse_product_line(product_line);
        auto fv = get_available_firmware_version(pl, pid);
        return !(fv == "");
    }

    int parse_product_line(const std::string& product_line)
    {
        if (product_line == "D400") return RS2_PRODUCT_LINE_D400;
        else return -1;
    }

    std::string get_available_firmware_version(int product_line, const std::string& pid)
    {
        if (product_line == RS2_PRODUCT_LINE_D400) return FW_D4XX_FW_IMAGE_VERSION;
        else return "";
    }

    std::vector< uint8_t > get_default_fw_image( int product_line, const std::string & pid )
    {
        std::vector< uint8_t > image;

        switch( product_line )
        {
        case RS2_PRODUCT_LINE_D400: 
        {
            bool allow_rc_firmware = config_file::instance().get_or_default( configurations::update::allow_rc_firmware, false );
            if( strlen( FW_D4XX_FW_IMAGE_VERSION ) && ! allow_rc_firmware )
            {
                int size = 0;
                auto hex = fw_get_D4XX_FW_Image( size );
                image = std::vector< uint8_t >( hex, hex + size );
            }
        }
        break;
        default:
            break;
        }
        return image;
    }

    bool is_upgradeable(const std::string& curr, const std::string& available)
    {
        if (curr == "" || available == "") return false;
        return rsutils::version( curr ) < rsutils::version( available );
    }

    bool firmware_update_manager::check_for(
        std::function<bool()> action, std::function<void()> cleanup,
        std::chrono::system_clock::duration delta)
    {
        using namespace std;
        using namespace std::chrono;

        auto start = system_clock::now();
        auto now = system_clock::now();
        do
        {
            try
            {

                if (action()) return true;
            }
            catch (const error& e)
            {
                fail(error_to_string(e));
                cleanup();
                return false;
            }
            catch (const std::exception& ex)
            {
                fail(ex.what());
                cleanup();
                return false;
            }
            catch (...)
            {
                fail("Unknown error during update.\nPlease reconnect the camera to exit recovery mode");
                cleanup();
                return false;
            }

            now = system_clock::now();
            this_thread::sleep_for(milliseconds(100));
        } while (now - start < delta);
        return false;
    }

    void firmware_update_manager::process_mipi()
    {
        bool is_mipi_recovery = !(strcmp(_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID), "BBCD"));
        if (!_is_signed)
        {
            fail("Signed FW update for MIPI device - This FW file is not signed ");
            return;
        }
        if (!is_mipi_recovery)
        {
            auto dev_updatable = _dev.as<updatable>();
            if(!(dev_updatable && dev_updatable.check_firmware_compatibility(_fw)))
            {
                std::stringstream ss;
                ss << "The firmware version is not compatible with ";
                ss << _dev.get_info(RS2_CAMERA_INFO_NAME) << std::endl;
                fail(ss.str());
                return;
            }
        }
        log("Burning Signed Firmware on MIPI device");
        if (!is_mipi_recovery)
        {
            // Enter DFU mode
            auto device_debug = _dev.as<rs2::debug_protocol>();
            uint32_t dfu_opcode = 0x1e;
            device_debug.build_command(dfu_opcode, 1);
        }
        _progress = 30;
        rs2_camera_info _dfu_port_info = (is_mipi_recovery)?(RS2_CAMERA_INFO_PHYSICAL_PORT):(RS2_CAMERA_INFO_DFU_DEVICE_PATH);
        // Write signed firmware to appropriate file descriptor
        std::ofstream fw_path_in_device(_dev.get_info(_dfu_port_info), std::ios::binary);
        if (fw_path_in_device)
        {
            fw_path_in_device.write(reinterpret_cast<const char*>(_fw.data()), _fw.size());
        }
        else
        {
            fail("Firmware Update failed - wrong path or permissions missing");
            return;
        }
        log("FW update process completed successfully.");
        LOG_INFO("FW update process completed successfully.");
        fw_path_in_device.close();

        _progress = 100;
        if (is_mipi_recovery)
        {
            log("For GMSL MIPI device please reboot, or reload d4xx driver\n"\
                "sudo rmmod d4xx && sudo modprobe d4xx\n"\
                "and restart the realsense-viewer");
            LOG_INFO("For GMSL MIPI device please reboot, or reload d4xx driver\n"\
                     "sudo rmmod d4xx && sudo modprobe d4xx\n"\
                     "and restart the realsense-viewer");
        }
        _done = true;
        // Restart the device to reconstruct with the new version information
        _dev.hardware_reset();
    }

    void firmware_update_manager::process_flow(
        std::function<void()> cleanup,
        invoker invoke)
    {
        // if device is MIPI device, and fw is signed - using mipi specific procedure
        if (_is_signed
                && (!strcmp(_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID), "ABCD")
                 || !strcmp(_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID), "BBCD")
                 || !strcmp(_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID), "ABCE"))
                )
        {
            process_mipi();
            return;
        }

        std::string serial = "";
        if (_dev.supports(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID))
            serial = _dev.get_info(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID);
        else
            serial = _dev.query_sensors().front().get_info(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID);

        // Clear FW update related notification to avoid dismissing the notification on ~device_model()
        // We want the notification alive during the whole process.
        _model.related_notifications.erase(
            std::remove_if( _model.related_notifications.begin(),
                            _model.related_notifications.end(),
                            []( std::shared_ptr< notification_model > n ) {
                                return n->is< fw_update_notification_model >();
                            } ) , end(_model.related_notifications));

        for (auto&& n : _model.related_notifications)
        {
            if (n->is< fw_update_notification_model >()
                || n->is< sw_recommended_update_alert_model >())
                n->dismiss(false);
        }

        _progress = 5;

        int next_progress = 10;

        update_device dfu{};

        if (auto upd = _dev.as<updatable>())
        {
            // checking firmware version compatibility with device
            if (_is_signed)
            {
                if (!upd.check_firmware_compatibility(_fw))
                {
                    std::stringstream ss;
                    ss << "The firmware version is not compatible with ";
                    ss << _dev.get_info(RS2_CAMERA_INFO_NAME) << std::endl;
                    fail(ss.str());
                    return;
                }
            }

            log( "Trying to back-up camera flash memory" );

            std::string log_backup_status;
            try
            {
                auto flash = upd.create_flash_backup( [&]( const float progress )
                    {
                        _progress = ( ( ceil( progress * 5 ) / 5 ) * ( 30 - next_progress ) ) + next_progress;
                    } );

                // Not all cameras supports this feature
                if( !flash.empty() )
                {
                    auto temp = rsutils::os::get_special_folder( rsutils::os::special_folder::app_data );
                    temp += serial + "." + get_timestamped_file_name() + ".bin";

                    {
                        std::ofstream file( temp.c_str(), std::ios::binary );
                        file.write( (const char*)flash.data(), flash.size() );
                        log_backup_status = "Backup completed and saved as '" + temp + "'";
                    }
                }
                else
                {
                    log_backup_status = "Backup flash is not supported";
                }
            }
            catch( const std::exception& e )
            {
                if( auto not_model_protected = get_protected_notification_model() )
                {
                    log_backup_status = "WARNING: backup failed; continuing without it...";
                    not_model_protected->output.add_log( RS2_LOG_SEVERITY_WARN,
                        __FILE__,
                        __LINE__,
                        log_backup_status + ", Error: " + e.what() );
                }
            }
            catch( ... )
            {
                if( auto not_model_protected = get_protected_notification_model() )
                {
                    log_backup_status = "WARNING: backup failed; continuing without it...";
                    not_model_protected->add_log( log_backup_status + ", Unknown error occurred" );
                }
            }

            if ( !log_backup_status.empty() )
                log(log_backup_status);

            next_progress = static_cast<int>(_progress) + 10;

            if (_is_signed)
            {
                log("Requesting to switch to recovery mode");

                // in order to update device to DFU state, it will be disconnected then switches to DFU state
                // if querying devices is called while device still switching to DFU state, an exception will be thrown
                // to prevent that, a blocking is added to make sure device is updated before continue to next step of querying device
                upd.enter_update_state();

                if (!check_for([this, serial, &dfu]() {
                    auto devs = _ctx.query_devices();

                    for (uint32_t j = 0; j < devs.size(); j++)
                    {
                        try
                        {
                            auto d = devs[j];
                            if (d.is<update_device>())
                            {
                                if (d.supports(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID))
                                {
                                    if (serial == d.get_info(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID))
                                    {
                                        log( "DFU device '" + serial + "' found" );
                                        dfu = d;
                                        return true;
                                    }
                                }
                            }
                        }
                        catch (std::exception &e) {
                            if (auto not_model_protected = get_protected_notification_model())
                            {
                                not_model_protected->output.add_log(RS2_LOG_SEVERITY_WARN,
                                    __FILE__,
                                    __LINE__,
                                    rsutils::string::from() << "Exception caught in FW Update process-flow: " << e.what() << "; Retrying...");
                            }
                        }
                        catch (...) {}
                    }

                    return false;
                }, cleanup, std::chrono::seconds(60)))
                {
                    fail("Recovery device did not connect in time!");
                    return;
                }
            }
        }
        else
        {
            dfu = _dev.as<update_device>();
        }

        if (dfu)
        {
            _progress = float(next_progress);

            log("Recovery device connected, starting update..\n"
                "Internal write is in progress\n"
                "Please DO NOT DISCONNECT the camera");

            dfu.update(_fw, [&](const float progress)
            {
                _progress = ((ceil(progress * 10) / 10 * (90 - next_progress)) + next_progress);
            });

            log( "Firmware Download completed, await DFU transition event" );
            std::this_thread::sleep_for( std::chrono::seconds( 3 ) );
        }
        else
        {
            auto upd = _dev.as<updatable>();
            upd.update_unsigned(_fw, [&](const float progress)
            {
                _progress = (ceil(progress * 10) / 10 * (90 - next_progress)) + next_progress;
            });
            log("Firmware Update completed, waiting for device to reconnect");
        }

        if (!check_for([this, serial]() {
            auto devs = _ctx.query_devices();

            for (uint32_t j = 0; j < devs.size(); j++)
            {
                try
                {
                    auto d = devs[j];

                    if (d.query_sensors().size() && d.query_sensors().front().supports(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID))
                    {
                        auto s = d.query_sensors().front().get_info(RS2_CAMERA_INFO_FIRMWARE_UPDATE_ID);
                        if (s == serial)
                        {
                            log("Discovered connection of the original device");
                            return true;
                        }
                    }
                }
                catch (...) {}
            }

            return false;
        }, cleanup, std::chrono::seconds(60)))
        {
            fail("Original device did not reconnect in time!");
            return;
        }

        log( "Device reconnected successfully!\n"
             "FW update process completed successfully" );

        _progress = 100;

        _done = true;
    }

    void fw_update_notification_model::draw_content(ux_window& win, int x, int y, float t, std::string& error_message)
    {
        using namespace std;
        using namespace chrono;

        ImGui::SetCursorScreenPos({ float(x + 9), float(y + 4) });

        ImVec4 shadow{ 1.f, 1.f, 1.f, 0.1f };
        ImGui::GetWindowDrawList()->AddRectFilled({ float(x), float(y) },
        { float(x + width), float(y + 25) }, ImColor(shadow));

        if (update_state != RS2_FWU_STATE_COMPLETE)
        {
            ImGui::Text("Firmware Update Recommended!");

            ImGui::SetCursorScreenPos({ float(x + 5), float(y + 27) });

            draw_text(get_title().c_str(), x, y, height - 50);

            ImGui::SetCursorScreenPos({ float(x + 9), float(y + height - 67) });

            ImGui::PushStyleColor(ImGuiCol_Text, alpha(light_grey, 1.f - t));

            if (update_state == RS2_FWU_STATE_INITIAL_PROMPT)
                ImGui::Text("Firmware updates offer critical bug fixes and\nunlock new camera capabilities.");
            else
                ImGui::Text("Firmware update is underway...\nPlease do not disconnect the device");

            ImGui::PopStyleColor();
        }
        else
        {
            //ImGui::PushStyleColor(ImGuiCol_Text, alpha(light_blue, 1.f - t));
            ImGui::Text("Update Completed");
            //ImGui::PopStyleColor();

            ImGui::SetCursorScreenPos({ float(x + 10), float(y + 35) });
            ImGui::PushFont(win.get_large_font());
            std::string txt = rsutils::string::from() << textual_icons::throphy;
            ImGui::Text("%s", txt.c_str());
            ImGui::PopFont();

            ImGui::SetCursorScreenPos({ float(x + 40), float(y + 35) });
            ImGui::Text("Camera Firmware Updated Successfully");
        }

        ImGui::SetCursorScreenPos({ float(x + 5), float(y + height - 25) });

        const auto bar_width = width - 115;

        if (update_manager)
        {
            if (update_state == RS2_FWU_STATE_INITIAL_PROMPT)
            {
                auto sat = 1.f + sin(duration_cast<milliseconds>(system_clock::now() - created_time).count() / 700.f) * 0.1f;
                ImGui::PushStyleColor(ImGuiCol_Button, saturate(sensor_header_light_blue, sat));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, saturate(sensor_header_light_blue, 1.5f));
                std::string button_name = rsutils::string::from() << "Install" << "##fwupdate" << index;

                if (ImGui::Button(button_name.c_str(), { float(bar_width), 20.f }) || update_manager->started())
                {
                    // stopping stream before starting fw update
                    auto fw_update_manager = dynamic_cast<firmware_update_manager*>(update_manager.get());
                    if( ! fw_update_manager )
                        throw std::runtime_error( "Cannot convert firmware_update_manager" );
                    std::for_each(fw_update_manager->get_device_model().subdevices.begin(),
                        fw_update_manager->get_device_model().subdevices.end(),
                        [&](const std::shared_ptr<subdevice_model>& sm)
                        {
                            if (sm->streaming)
                            {
                                try
                                {
                                    sm->stop(fw_update_manager->get_protected_notification_model());
                                }
                                catch (...)
                                {
                                    // avoiding exception that can be sent by stop method
                                    // this could happen if the sensor is not streaming and the stop method is called - for example
                                }
                            }
                        });

                    auto _this = shared_from_this();
                    auto invoke = [_this](std::function<void()> action) {
                        _this->invoke(action);
                    };

                    if (!update_manager->started())
                        update_manager->start(invoke);

                    update_state = RS2_FWU_STATE_IN_PROGRESS;
                    enable_dismiss = false;
                    _progress_bar.last_progress_time = system_clock::now();
                }
                ImGui::PopStyleColor(2);

                if (ImGui::IsItemHovered())
                {
                    RsImGui::CustomTooltip("%s", "New firmware will be flashed to the device");
                }
            }
            else if (update_state == RS2_FWU_STATE_IN_PROGRESS)
            {
                if (update_manager->done())
                {
                    update_state = RS2_FWU_STATE_COMPLETE;
                    pinned = false;
                    _progress_bar.last_progress_time = last_interacted = system_clock::now();
                }

                if (!expanded)
                {
                    if (update_manager->failed())
                    {
                        update_manager->check_error(error_message);
                        update_state = RS2_FWU_STATE_FAILED;
                        pinned = false;
                        dismiss(false);
                    }

                    draw_progress_bar(win, bar_width);

                    ImGui::SetCursorScreenPos({ float(x + width - 105), float(y + height - 25) });

                    string id = rsutils::string::from() << "Expand" << "##" << index;
                    ImGui::PushStyleColor(ImGuiCol_Text, light_grey);
                    if (ImGui::Button(id.c_str(), { 100, 20 }))
                    {
                        expanded = true;
                    }

                    ImGui::PopStyleColor();
                }
            }
        }
        else
        {
            std::string button_name = rsutils::string::from() << "Learn More..." << "##" << index;

            if (ImGui::Button(button_name.c_str(), { float(bar_width), 20 }))
            {
                open_url(recommended_fw_url);
            }
            if (ImGui::IsItemHovered())
            {
                win.link_hovered();
                RsImGui::CustomTooltip("%s", "Internet connection required");
            }
        }
    }

    void fw_update_notification_model::draw_expanded(ux_window& win, std::string& error_message)
    {
        if (update_manager->started() && update_state == RS2_FWU_STATE_INITIAL_PROMPT)
            update_state = RS2_FWU_STATE_IN_PROGRESS;

        auto flags = ImGuiWindowFlags_NoResize |
            ImGuiWindowFlags_NoMove |
            ImGuiWindowFlags_NoCollapse;

        ImGui::PushStyleColor(ImGuiCol_WindowBg, { 0, 0, 0, 0 });
        ImGui::PushStyleColor(ImGuiCol_Text, light_grey);
        ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, white);
        ImGui::PushStyleColor(ImGuiCol_PopupBg, sensor_bg);

        ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(500, 100));
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5, 5));
        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);

        std::string title = "Firmware Update";
        if (update_manager->failed()) title += " Failed";

        ImGui::OpenPopup(title.c_str());
        if (ImGui::BeginPopupModal(title.c_str(), nullptr, flags))
        {
            ImGui::SetCursorPosX(200);
            std::string progress_str = rsutils::string::from() << "Progress: " << update_manager->get_progress() << "%";
            ImGui::Text("%s", progress_str.c_str());

            ImGui::SetCursorPosX(5);

            draw_progress_bar(win, 490);

            ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, regular_blue);
            auto s = update_manager->get_log();
            ImGui::InputTextMultiline("##fw_update_log", const_cast<char*>(s.c_str()),
                s.size() + 1, { 490,100 }, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_ReadOnly);
            ImGui::PopStyleColor();

            ImGui::SetCursorPosX(190);
            if (visible || update_manager->done() || update_manager->failed())
            {
                if (ImGui::Button("OK", ImVec2(120, 0)))
                {
                    if (update_manager->done() || update_manager->failed())
                    {
                        update_state = RS2_FWU_STATE_FAILED;
                        pinned = false;
                        dismiss(false);
                    }
                    expanded = false;
                    ImGui::CloseCurrentPopup();
                }
            }
            else
            {
                ImGui::PushStyleColor(ImGuiCol_Button, transparent);
                ImGui::PushStyleColor(ImGuiCol_ButtonActive, transparent);
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, transparent);
                ImGui::PushStyleColor(ImGuiCol_Text, transparent);
                ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, transparent);
                ImGui::Button("OK", ImVec2(120, 0));
                ImGui::PopStyleColor(5);
            }

            ImGui::EndPopup();
        }

        ImGui::PopStyleVar(3);
        ImGui::PopStyleColor(4);

        error_message = "";
    }

    int fw_update_notification_model::calc_height()
    {
        if (update_state != RS2_FWU_STATE_COMPLETE) return 150;
        else return 65;
    }

    void fw_update_notification_model::set_color_scheme(float t) const
    {
        notification_model::set_color_scheme(t);

        ImGui::PopStyleColor(1);

        ImVec4 c;

        if (update_state == RS2_FWU_STATE_COMPLETE)
        {
            c = alpha(saturate(light_blue, 0.7f), 1 - t);
            ImGui::PushStyleColor(ImGuiCol_WindowBg, c);
        }
        else
        {
            c = alpha(sensor_bg, 1 - t);
            ImGui::PushStyleColor(ImGuiCol_WindowBg, c);
        }
    }

    fw_update_notification_model::fw_update_notification_model(std::string name,
        std::shared_ptr<firmware_update_manager> manager, bool exp)
        : process_notification_model(manager)
    {
        enable_expand = false;
        expanded = exp;
        if (expanded) visible = false;

        message = name;
        this->severity = RS2_LOG_SEVERITY_INFO;
        this->category = RS2_NOTIFICATION_CATEGORY_FIRMWARE_UPDATE_RECOMMENDED;
        pinned = true;
        forced = true;
    }
}
